동시 출현 단어 분석(Co-occurrence analysis)

기본적인 전처리

# 기생충 기사 댓글 불러오기
library(readr)
raw_news_comment <- read_csv("news_comment_parasite.csv")

# 전처리
library(dplyr)
library(stringr)
library(textclean)

news_comment <- raw_news_comment %>%
  select(reply) %>%
  mutate(reply = str_replace_all(reply, "[^가-힣]", " "),
         reply = str_squish(reply),
         id = row_number())

토큰화하기

1. 형태소 분석기를 이용해 품사 기준으로 토큰화하기
  • SimplePos22() : 문장의 단어를 22개의 품사로 구분
library(tidytext)
library(KoNLP)

comment_pos <- news_comment %>%
  unnest_tokens(input = reply,
                output = word,
                token = SimplePos22,
                drop = F)

comment_pos %>%
  select(word, reply)
## # A tibble: 39,956 x 2
##    word          reply                                                         
##    <chr>         <chr>                                                         
##  1 정말/ma       정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
##  2 우리/np       정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
##  3 집/nc+에/jc   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
##  4 좋/pa+은/et   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
##  5 일/nc+이/jc   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
##  6 생기/pv+어/ec 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
##  7 기쁘/pa+고/ec 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
##  8 행복한/nc     정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
##  9 것/nb+처럼/jc 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
## 10 나/np+의/jc   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 ~
## # ... with 39,946 more rows
품사 분리하여 행 구성하기
  • 원하는 품사를 추출하기 쉽도록 한 행을 한 품사로 구성하기

  • tidyr::separate_rows():

    • 정규 표현식에 따라 텍스트를 여러 행으로 나누기

    • sep = "[+]": "+"가 등장할 때마다 행을 나눔

# 품사별로 행 분리
library(tidyr)
comment_pos <- comment_pos %>%
  separate_rows(word, sep = "[+]")

comment_pos %>%
  select(word, reply)
## # A tibble: 66,898 x 2
##    word    reply                                                                
##    <chr>   <chr>                                                                
##  1 정말/ma 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  2 우리/np 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  3 집/nc   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  4 에/jc   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  5 좋/pa   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  6 은/et   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  7 일/nc   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  8 이/jc   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  9 생기/pv 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 10 어/ec   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## # ... with 66,888 more rows
3. 품사 추출하기
(1) 명사 추출하기
  • "/n"이 붙어있는 단어 추출
  • 태그 제거: ‘/로 시작하는 모든 문자’ 제거
# 명사 추출하기
noun <- comment_pos %>%
  filter(str_detect(word, "/n")) %>%
  mutate(word = str_remove(word, "/.*$"))

noun %>%
  select(word, reply)
## # A tibble: 28,158 x 2
##    word   reply                                                                
##    <chr>  <chr>                                                                
##  1 우리   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  2 집     정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  3 일     정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  4 행복한 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  5 것     정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  6 나     정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  7 일     정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  8 양     정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
##  9 행복   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## 10 행복   정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 행복~
## # ... with 28,148 more rows
# 명사 빈도 구하기
noun %>%
  count(word, sort = T)
## # A tibble: 8,866 x 2
##    word         n
##    <chr>    <int>
##  1 영화       460
##  2 기생충     443
##  3 봉준호     339
##  4 것         328
##  5 축하       260
##  6 아카데미   252
##  7 대단       241
##  8 나         225
##  9 대한민국   225
## 10 자랑       217
## # ... with 8,856 more rows
(2) 동사, 형용사 추출하기
  • 동사 "/pv", 형용사:"/pa" 붙어있는 단어 추출

  • 단어 뒤에 태그 대신 ’다’를 붙여 이해하기 편하게 수정하기

    • ex) “받” → “받다”, “멋지” → “멋지다”
# 동사, 형용사 추출하기
pvpa <- comment_pos %>%
  filter(str_detect(word, "/pv|/pa")) %>%         # "/pv", "/pa" 추출
  mutate(word = str_replace(word, "/.*$", "다"))  # "/"로 시작 문자를 "다"로 바꾸기

pvpa %>%
  select(word, reply)
## # A tibble: 4,977 x 2
##    word      
##    <chr>     
##  1 좋다      
##  2 생기다    
##  3 기쁘다    
##  4 축하드리다
##  5 기쁘다    
##  6 기쁘다    
##  7 기쁘다    
##  8 축하드리다
##  9 불다      
## 10 크다      
##    reply                                                    
##    <chr>                                                    
##  1 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 ~
##  2 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 ~
##  3 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 ~
##  4 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 ~
##  5 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 ~
##  6 와 너무 기쁘다 이 시국에 정말 내 일같이 기쁘고 감사하다 ~
##  7 와 너무 기쁘다 이 시국에 정말 내 일같이 기쁘고 감사하다 ~
##  8 와 너무 기쁘다 이 시국에 정말 내 일같이 기쁘고 감사하다 ~
##  9 우리나라의 영화감독분들 그리고 앞으로 그 꿈을 그리는 분~ 
## 10 우리나라의 영화감독분들 그리고 앞으로 그 꿈을 그리는 분~ 
## # ... with 4,967 more rows
(3) 추출한 데이터 결합하기
  • 추출한 단어 결합하기

  • 이해할 수 있는 두 글자 이상 단어만 남기기

# 품사 결합
comment <- bind_rows(noun, pvpa) %>%
  filter(str_count(word) >= 2) %>%
  arrange(id)

comment %>%
  select(word, reply)
## # A tibble: 27,535 x 2
##    word       reply                                                            
##    <chr>      <chr>                                                            
##  1 우리       정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
##  2 행복한     정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
##  3 행복       정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
##  4 행복       정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
##  5 좋다       정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
##  6 생기다     정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
##  7 기쁘다     정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
##  8 축하드리다 정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
##  9 기쁘다     정말 우리 집에 좋은 일이 생겨 기쁘고 행복한 것처럼 나의 일인 양 ~
## 10 시국       와 너무 기쁘다 이 시국에 정말 내 일같이 기쁘고 감사하다 축하드려~
## # ... with 27,525 more rows
명사, 동사, 형용사를 한 번에 추출하기
  • 명사, 동사, 형용사를 추출해 결합한 후 두 글자 이상만 남기기
comment_new <- comment_pos %>%
  separate_rows(word, sep = "[+]") %>%
  filter(str_detect(word, "/n|/pv|/pa")) %>%
  mutate(word = ifelse(str_detect(word, "/pv|/pa"),
                       str_replace(word, "/.*$", "다"),
                       str_remove(word, "/.*$"))) %>%
  filter(str_count(word) >= 2) %>%
  arrange(id)

]

단어 동시 출현 빈도 구하기

install.packages("widyr")
library(widyr)

pair <- comment %>%
  pairwise_count(item = word,
                 feature = id,
                 sort = T)
pair
## # A tibble: 263,462 x 3
##    item1      item2          n
##    <chr>      <chr>      <dbl>
##  1 영화       기생충       111
##  2 기생충     영화         111
##  3 감독       봉준호        86
##  4 봉준호     감독          86
##  5 감독님     봉준호        66
##  6 봉준호     감독님        66
##  7 만들다     영화          55
##  8 영화       만들다        55
##  9 블랙리스트 감독          54
## 10 감독       블랙리스트    54
## # ... with 263,452 more rows
특정 단어와 자주 함께 사용된 단어 살펴보기
pair %>% filter(item1 == "영화")
## # A tibble: 2,429 x 3
##    item1 item2        n
##    <chr> <chr>    <dbl>
##  1 영화  기생충     111
##  2 영화  만들다      55
##  3 영화  봉준호      47
##  4 영화  받다        45
##  5 영화  아카데미    42
##  6 영화  같다        41
##  7 영화  감독        39
##  8 영화  아니다      38
##  9 영화  한국        34
## 10 영화  좋다        33
## # ... with 2,419 more rows
pair %>% filter(item1 == "봉준호")
## # A tibble: 1,491 x 3
##    item1  item2          n
##    <chr>  <chr>      <dbl>
##  1 봉준호 감독          86
##  2 봉준호 감독님        66
##  3 봉준호 기생충        49
##  4 봉준호 영화          47
##  5 봉준호 블랙리스트    43
##  6 봉준호 대한민국      37
##  7 봉준호 자랑          29
##  8 봉준호 대단          28
##  9 봉준호 축하          27
## 10 봉준호 아카데미      24
## # ... with 1,481 more rows
동시 출현 네트워크(co-occurrence network)
  • 동시 출현 빈도를 이용해 단어의 관계를 네트워크 형태로 표현

  • 단어들이 어떤 맥락에서 함께 사용되었는지 이해할 수 있다

네트워크 그래프 데이터 만들기

네트워크가 너무 복잡하지 않도록 25회 이상 사용된 단어 추출해 생성

install.packages("tidygraph")
library(tidygraph)

graph_comment <- pair %>%
  filter(n >= 25) %>%
  as_tbl_graph()

graph_comment
## # A tbl_graph: 26 nodes and 88 edges
## #
## # A directed simple graph with 2 components
## #
## # Node Data: 26 x 1 (active)
##   name  
##   <chr> 
## 1 영화  
## 2 기생충
## 3 감독  
## 4 봉준호
## 5 감독님
## 6 만들다
## # ... with 20 more rows
## #
## # Edge Data: 88 x 3
##    from    to     n
##   <int> <int> <dbl>
## 1     1     2   111
## 2     2     1   111
## 3     3     4    86
## # ... with 85 more rows

네트워크 그래프 만들기

install.packages("ggraph")
library(ggraph)

ggraph(graph_comment) +
  geom_edge_link() +                 # 엣지
  geom_node_point() +                # 노드
  geom_node_text(aes(label = name))  # 텍스트

그래프를 큰 화면에 출력하는 방법
  • Plots 창의 Zoom 아이콘 클릭

이미지 출력 창을 별도로 열어 큰 화면에서 보기 - 윈도우: windows() - macOS: x11()

그래프 다듬기
# 한글 폰트 설정
library(showtext)
font_add_google(name = "Nanum Gothic", family = "nanumgothic")
showtext_auto()
엣지와 노드의 색깔, 크기, 텍스트 위치 수정
  • ggraph(layout = "fr"): 네트워크 형태 결정

    • 난수를 이용해 매번 형태 달라짐 → set.seed()로 난수 고정
set.seed(1234)                              # 난수 고정
ggraph(graph_comment, layout = "fr") +      # 레이아웃

  geom_edge_link(color = "gray50",          # 엣지 색깔
                 alpha = 0.5) +             # 엣지 명암

  geom_node_point(color = "lightcoral",     # 노드 색깔
                  size = 5) +               # 노드 크기

  geom_node_text(aes(label = name),         # 텍스트 표시
                 repel = T,                 # 노드밖 표시
                 size = 5,                  # 텍스트 크기
                 family = "nanumgothic") +  # 폰트

  theme_graph()                             # 배경 삭제

노드 텍스트 폰트 geom_node_text()family로 별도 설정. theme()으로 적용 안됨.

네트워크 그래프 함수 만들기
word_network <- function(x) {
  ggraph(x, layout = "fr") +
    geom_edge_link(color = "gray50",
                   alpha = 0.5) +
    geom_node_point(color = "lightcoral",
                    size = 5) +
    geom_node_text(aes(label = name),
                   repel = T,
                   size = 5,
                   family = "nanumgothic") +
    theme_graph()
}
set.seed(1234)
word_network(graph_comment)

유의어 처리하기

library(ggraph)
# 유의어 처리하기
comment <- comment %>%
  mutate(word = ifelse(str_detect(word, "감독") &
                      !str_detect(word, "감독상"), "봉준호", word),
         word = ifelse(word == "오르다", "올리다", word),
         word = ifelse(str_detect(word, "축하"), "축하", word))

# 단어 동시 출현 빈도 구하기
pair <- comment %>%
  pairwise_count(item = word,
                 feature = id,
                 sort = T)

# 네트워크 그래프 데이터 만들기
graph_comment <- pair %>%
  filter(n >= 25) %>%
  as_tbl_graph()

# 네트워크 그래프 만들기
set.seed(1234)
word_network(graph_comment)

연결 중심성과 커뮤니티 표현하기

연결 중심성(degree centrality)
  • 노드가 다른 노드들과 얼마나 밀접하게 연결되는지 나타낸 값

  • 연결 중심성으로 노드 크기를 조정하면 어떤 단어를 눈여겨봐야 할지 판단하기 쉬워진다

연결 중심성과 커뮤니티 표현하기

커뮤니티(community)
  • 단어 간의 관계가 가까워 빈번하게 연결된 노드 집단

  • 노드를 커뮤니티별로 구분 지어 서로 다른 색으로 표현하면 네트워크 구조를 이해하기 쉬워진다

1. 네트워크 그래프 데이터에 연결 중심성, 커뮤니티 변수 추가하기
  • 네트워크 그래프 데이터 만들기: as_tbl_graph()
    • directed = F: 방향성 없도록 설정
    • group_infomap()은 방향성 없는 네트워크 그래프 데이터에서만 커뮤니티를 찾아줌
  • 연결 중심성 변수 추가하기: centrality_degree()
  • 커뮤니티 변수 추가하기: group_infomap()
    • 커뮤니티가 정수형 숫자이므로 노드가 그라데이션으로 표현됨
    • as.factor(): factor 타입으로 변환해 노드 그룹별로 다른 색으로 표현
set.seed(1234)
graph_comment <- pair %>%
  filter(n >= 25) %>%
  as_tbl_graph(directed = F) %>%
  mutate(centrality = centrality_degree(),    # 연결 중심성
         group = as.factor(group_infomap()))  # 커뮤니티

graph_comment
## # A tbl_graph: 33 nodes and 144 edges
## #
## # An undirected multigraph with 1 component
## #
## # Node Data: 33 x 3 (active)
##   name       centrality group
##   <chr>           <dbl> <fct>
## 1 봉준호             58 1    
## 2 축하               32 2    
## 3 영화               26 4    
## 4 기생충             24 3    
## 5 블랙리스트          6 5    
## 6 자랑                6 2    
## # ... with 27 more rows
## #
## # Edge Data: 144 x 3
##    from    to     n
##   <int> <int> <dbl>
## 1     1     2   197
## 2     1     2   197
## 3     1     3   113
## # ... with 141 more rows

2. 네트워크 그래프에 연결 중심성, 커뮤니티 표현하기

set.seed(1234)
ggraph(graph_comment, layout = "fr") +      # 레이아웃

  geom_edge_link(color = "gray50",          # 엣지 색깔
                 alpha = 0.5) +             # 엣지 명암

  geom_node_point(aes(size = centrality,    # 노드 크기
                      color = group),       # 노드 색깔
                  show.legend = F) +        # 범례 삭제
  scale_size(range = c(5, 15)) +            # 노드 크기 범위

  geom_node_text(aes(label = name),         # 텍스트 표시
                 repel = T,                 # 노드밖 표시
                 size = 5,                  # 텍스트 크기
                 family = "nanumgothic") +  # 폰트

  theme_graph()                             # 배경 삭제

3. 네트워크의 주요 단어 살펴보기

주요 단어의 커뮤니티 살펴보기
graph_comment %>%
  filter(name == "봉준호")
## # A tbl_graph: 1 nodes and 0 edges
## #
## # An unrooted tree
## #
## # Node Data: 1 x 3 (active)
##   name   centrality group
##   <chr>       <dbl> <fct>
## 1 봉준호         58 1    
## #
## # Edge Data: 0 x 3
## # ... with 3 variables: from <int>, to <int>, n <dbl>

같은 커뮤니티로 분류된 단어 살펴보기

graph_comment %>%
  filter(group == 4) %>%
  arrange(-centrality) %>%
  data.frame()
##       name centrality group
## 1     영화         26     4
## 2 아카데미         10     4
## 3   만들다          4     4

연결 중심성이 높은 주요 단어 살펴보기

graph_comment %>%
  arrange(-centrality)
## # A tbl_graph: 33 nodes and 144 edges
## #
## # An undirected multigraph with 1 component
## #
## # Node Data: 33 x 3 (active)
##   name     centrality group
##   <chr>         <dbl> <fct>
## 1 봉준호           58 1    
## 2 축하             32 2    
## 3 영화             26 4    
## 4 기생충           24 3    
## 5 작품상           14 6    
## 6 대한민국         10 1    
## # ... with 27 more rows
## #
## # Edge Data: 144 x 3
##    from    to     n
##   <int> <int> <dbl>
## 1     1     2   197
## 2     1     2   197
## 3     1     3   113
## # ... with 141 more rows
graph_comment %>%
  filter(group == 2) %>%
  arrange(-centrality) %>%
  data.frame()
##     name centrality group
## 1   축하         32     2
## 2   자랑          6     2
## 3   같다          6     2
## 4 아니다          6     2
## 5   수상          4     2
## 6 멋지다          4     2
## 7   한국          2     2
## 8 기쁘다          2     2

4. 주요 단어가 사용된 원문 살펴보기

news_comment %>%
  filter(str_detect(reply, "봉준호") & str_detect(reply, "대박")) %>%
  select(reply)
## # A tibble: 19 x 1
##    reply                                          
##    <chr>                                          
##  1 대박 대박 진짜 대박 봉준호 감독님과 우리 배우~ 
##  2 내가 죽기전에 아카데미에서 한국어를 들을줄이야~
##  3 대박 관왕이라니 축하합니다 봉준호를 배출한 충~ 
##  4 우와 대박 진자 대단하다 봉준호                 
##  5 봉준호 경사났네 대박중에 대에박 축하합니다     
##  6 봉준호 작품상 탔다 대박                        
##  7 봉준호 군대 면제시켜도될듯 대박 여윽시 위대한 ~
##  8 아니 다른상을 받은것도 충분히 대단하고 굉장하~ 
##  9 봉준호 군대 면제시켜도될듯 대박 여윽시 위대한 ~
## 10 봉준호감독님대박 축하합니다                    
## # ... with 9 more rows
동시 출현 빈도의 한계
  • 대부분의 단어와 자주 함께 사용되는 단어쌍 다수

    • ex) "영화"-"기생충"
  • 다른 단어에 비해 상대적으로 자주 함께 사용된 단어가 무엇인지 살펴봐야 한다

파이 계수(phi coefficient)
  • 두 단어가 함께 사용되는 경우가 각각 사용되는 경우에 비해 얼마나 많은지 나타낸 지표

  • 상대적으로 관련성이 큰 단어 파악하는데 활용

  • 어떤 단어와 자주 함께 사용되지만 다른 단어와는 자주 함께 사용되지 않는 단어

파이 계수의 의미
  • X, Y 두 단어가 있을 때, 여러 텍스트에서 두 단어의 사용 여부를 놓고 가능한 모든 경우

    • X, Y 모두 있음( \(a\) )

    • X, Y 모두 없음( \(d\) )

    • X만 있음( \(b\) )

    • Y만 있음( \(c\) )

\[\phi=\frac{ad-bc}{\sqrt{(a+b)(c+d)(a+c)(b+d)}}\]

  • -1 ~ +1

    • +1에 가까울수록 두 단어가 자주 함께 사용되어 관련성이 크다는 의미

    • -1에 가까울수록 함께 사용되는 경우가 드물어 관련성이 작다는 의미

파이 계수 구하기

word_cors <- comment %>%
  add_count(word) %>%
  filter(n >= 20) %>%
  pairwise_cor(item = word, 
               feature = id, 
               sort = T)

word_cors

원자료에 빈도 나타낸 변수 추가

## # A tibble: 25,440 x 3
##    item1      item2      correlation
##    <chr>      <chr>            <dbl>
##  1 올리다     블랙리스트       0.476
##  2 블랙리스트 올리다           0.476
##  3 역사       쓰다             0.345
##  4 쓰다       역사             0.345
##  5 박근혜     블랙리스트       0.314
##  6 블랙리스트 박근혜           0.314
##  7 정경심     조국             0.305
##  8 조국       정경심           0.305
##  9 가족       조국             0.302
## 10 조국       가족             0.302
## # ... with 25,430 more rows

특정 단어와 관련성이 큰 단어 살펴보기

word_cors %>%
  filter(item1 == "대한민국")
## # A tibble: 159 x 3
##    item1    item2    correlation
##    <chr>    <chr>          <dbl>
##  1 대한민국 국민          0.189 
##  2 대한민국 자랑          0.156 
##  3 대한민국 위상          0.149 
##  4 대한민국 위대한        0.100 
##  5 대한민국 세계          0.0911
##  6 대한민국 문화          0.0758
##  7 대한민국 나라          0.0740
##  8 대한민국 감사합니      0.0725
##  9 대한민국 오늘          0.0716
## 10 대한민국 영화계        0.0685
## # ... with 149 more rows
word_cors %>%
  filter(item1 == "역사")
## # A tibble: 159 x 3
##    item1 item2    correlation
##    <chr> <chr>          <dbl>
##  1 역사  쓰다          0.345 
##  2 역사  최초          0.121 
##  3 역사  한국          0.113 
##  4 역사  영광          0.0724
##  5 역사  아니다        0.0721
##  6 역사  한국영화      0.0719
##  7 역사  이것          0.0617
##  8 역사  영화제        0.0617
##  9 역사  소름          0.0594
## 10 역사  감사          0.0589
## # ... with 149 more rows

파이 계수로 막대 그래프 만들기

1. 관심 단어별로 파이 계수가 큰 단어 추출하기
# 관심 단어 목록 생성
target <- c("대한민국", "역사", "수상소감", "봉준호")

top_cors <- word_cors %>%
  filter(item1 %in% target) %>%
  group_by(item1) %>%
  slice_max(correlation, n = 8)
2. 막대 그래프 만들기
# 그래프 순서 정하기
top_cors$item1 <- factor(top_cors$item1, levels = target)

library(ggplot2)
ggplot(top_cors, aes(x = reorder_within(item2, correlation, item1),
                 y = correlation,
                 fill = item1)) +
  geom_col(show.legend = F) +
  facet_wrap(~ item1, scales = "free") +
  coord_flip() +
  scale_x_reordered() +
  labs(x = NULL) +
  theme(text = element_text(family = "nanumgothic"))

파이 계수로 네트워크 그래프 만들기

1. 네트워크 그래프 데이터 만들기. 연결 중심성과 그룹 추가하기
set.seed(1234)
graph_cors <- word_cors %>%
  filter(correlation >= 0.15) %>%
  as_tbl_graph(directed = F) %>%
  mutate(centrality = centrality_degree(),
         group = as.factor(group_components()))

graph_cors %>% 
  activate(nodes) %>% 
  as_tibble() %>%  
  arrange(group,-centrality)
## # A tibble: 58 x 3
##    name       centrality group
##    <chr>           <dbl> <fct>
##  1 블랙리스트         12 1    
##  2 박근혜              8 1    
##  3 송강호              8 1    
##  4 올리다              4 1    
##  5 이미경              4 1    
##  6 봉준호              2 1    
##  7 자한당              2 1    
##  8 정권                2 1    
##  9 배우                2 1    
## 10 높다                6 2    
## # ... with 48 more rows
2. 네트워크 그래프 만들기
set.seed(1234)
ggraph(graph_cors, layout = "fr") +

  geom_edge_link(color = "gray50",
                 aes(edge_alpha = correlation,   # 엣지 명암
                     edge_width = correlation),  # 엣지 두께
                 show.legend = F) +              # 범례 삭제
  scale_edge_width(range = c(1, 4)) +            # 엣지 두께 범위

  geom_node_point(aes(size = centrality,
                      color = group),
                  show.legend = F) +
  scale_size(range = c(5, 10)) +

  geom_node_text(aes(label = name),
                 repel = T,
                 size = 5,
                 family = "nanumgothic") +

  theme_graph()

동시 출현 빈도, 파이 계수로 만든 네트워크 그래프의 차이점
  • 동시 출현 빈도를 이용한 네트워크 그래프

    • 여러 단어와 자주 함께 사용된 단어쌍 중심으로 네트워크 형성

    • 노드 대부분이 서로 연결되어 구조가 복잡하고 군집이 잘 드러나지 않음

    • 자주 사용된 단어를 파악할 때 활용

  • 파이 계수를 이용한 네트워크 그래프

    • 다른 단어에 비해 상대적으로 자주 함께 사용된 단어쌍 중심으로 네트워크 형성

    • 관련성이 큰 단어끼리만 연결되어 단어 군집이 명확하게 드러남

    • 밀접하게 관련된 단어쌍 파악할 때 활용

연이어 사용된 단어쌍 분석: n-gram

엔그램(n-gram)

  • 연이어 사용된 n개의 단어

    • 두 단어 연속: 바이그램(bigram) 또는 2-gram

    • 세 단어 연속: 트라이그램(trigram) 또는 3-gram

.center[ ]

  • 텍스트를 엔그램으로 토큰화하면

    • 단어 앞뒤에 연이어 사용된 단어를 함께 살펴봄: 얼마나 자주 ‘연이어’ 사용된 단어쌍인가?

    • 단어가 연결될 때 생기는 의미와 맥락을 이해할 수 있음

    • 대다수의 텍스트에 사용된 평범한 단어쌍이 아니라 분명한 의미를 드러내는 단어쌍 발견

엔그램으로 토큰화하기

샘플 텍스트로 엔그램 토큰화해보기

  • tidytext::unnest_tokens()
    • token = "ngrams"
    • n: 기준 단어 수
text <- tibble(value = "대한민국은 민주공화국이다. 대한민국의 주권은 국민에게 있고, 모든 권력은 국민으로부터 나온다.")

text
## # A tibble: 1 x 1
##   value                                                                                       
##   <chr>                                                                                       
## 1 대한민국은 민주공화국이다. 대한민국의 주권은 국민에게 있고, 모든 권력은 국민으로부터 나온다.
# 바이그램 토큰화
text %>%
  unnest_tokens(input = value,
                output = word,
                token = "ngrams",
                n = 2)  #<<
## # A tibble: 9 x 1
##   word                     
##   <chr>                    
## 1 대한민국은 민주공화국이다
## 2 민주공화국이다 대한민국의
## 3 대한민국의 주권은        
## 4 주권은 국민에게          
## 5 국민에게 있고            
## 6 있고 모든                
## 7 모든 권력은              
## 8 권력은 국민으로부터      
## 9 국민으로부터 나온다
# 트라이그램 토큰화
text %>%
  unnest_tokens(input = value,
                output = word,
                token = "ngrams",
                n = 3) #<<
## # A tibble: 8 x 1
##   word                                
##   <chr>                               
## 1 대한민국은 민주공화국이다 대한민국의
## 2 민주공화국이다 대한민국의 주권은    
## 3 대한민국의 주권은 국민에게          
## 4 주권은 국민에게 있고                
## 5 국민에게 있고 모든                  
## 6 있고 모든 권력은                    
## 7 모든 권력은 국민으로부터            
## 8 권력은 국민으로부터 나온다
  • 단어 기준 토큰화 = 유니그램(unigram) 토큰화
# 단어 기준 토큰화
text %>%
  unnest_tokens(input = value,
                output = word,
                token = "words") #<<
## # A tibble: 10 x 1
##    word          
##    <chr>         
##  1 대한민국은    
##  2 민주공화국이다
##  3 대한민국의    
##  4 주권은        
##  5 국민에게      
##  6 있고          
##  7 모든          
##  8 권력은        
##  9 국민으로부터  
## 10 나온다
# 유니그램 토큰화
text %>%
  unnest_tokens(input = value,
                output = word,
                token = "ngrams", #<<
                n = 1)            #<<
## # A tibble: 10 x 1
##    word          
##    <chr>         
##  1 대한민국은    
##  2 민주공화국이다
##  3 대한민국의    
##  4 주권은        
##  5 국민에게      
##  6 있고          
##  7 모든          
##  8 권력은        
##  9 국민으로부터  
## 10 나온다

기사 댓글로 바이그램 만들기

(1) 명사, 동사, 형용사 추출하기
  • comment_pos 이용: 댓글을 형태소로 토큰화 후 품사별로 행 분리

  • 명사, 동사, 형용사를 추출해 결합한 후 두 글자 이상만 남김

comment_new <- comment_pos %>%
  separate_rows(word, sep = "[+]") %>%
  filter(str_detect(word, "/n|/pv|/pa")) %>%
  mutate(word = ifelse(str_detect(word, "/pv|/pa"),
                       str_replace(word, "/.*$", "다"),
                       str_remove(word, "/.*$"))) %>%
  filter(str_count(word) >= 2) %>%
  arrange(id)

바이그램으로 토큰화할 때는 형태소 추출 먼저

  • 텍스트 원문을 바이그램으로 토큰화하면 원형은 같지만 표현만 다른 단어들이 개별 단어로 취급됨

    • ex) ‘하다’, ‘했다’, ‘하며’, ‘하므로’
  • 표현이 아니라 의미 중심으로 분석해야 하므로 형태소를 먼저 추출한 다음 바이그램으로 토큰화해야 함

(2) 유의어 처리하기
comment_new <- comment_new %>%
  mutate(word = ifelse(str_detect(word, "감독") &
                      !str_detect(word, "감독상"), "봉준호", word),
         word = ifelse(word  == "오르다", "올리다", word),
         word = ifelse(str_detect(word, "축하"), "축하", word))
(3) 한 댓글이 하나의 행이 되도록 결합하기
comment_new %>%
  select(word)
## # A tibble: 27,535 x 1
##    word  
##    <chr> 
##  1 우리  
##  2 좋다  
##  3 생기다
##  4 기쁘다
##  5 행복한
##  6 행복  
##  7 축하  
##  8 행복  
##  9 기쁘다
## 10 기쁘다
## # ... with 27,525 more rows
(3) 한 댓글이 하나의 행이 되도록 결합하기
line_comment <- comment_new %>%
  group_by(id) %>%
  summarise(sentence = paste(word, collapse = " "))

line_comment
## # A tibble: 4,024 x 2
##       id sentence                                                               
##    <int> <chr>                                                                  
##  1     1 우리 좋다 생기다 기쁘다 행복한 행복 축하 행복 기쁘다                   
##  2     2 기쁘다 시국 기쁘다 감사 축하 진심으                                    
##  3     3 우리나라 봉준호 불다 크다 영감 봉준호 공동각본쓴 한진원 작가님 축하 축~
##  4     4 봉준호 봉준호 우리나라 대한민국 자랑 세계 어디 우리 한국인 힘내다 삽시 
##  5     5 노벨상 탄느낌이네요 축하 합니                                          
##  6     6 기생충 받다 박수 치다 감독상 기대다 봉준호 봉준호                      
##  7     7 대한민국 영화사 쓰다 계시다                                            
##  8     8 저런게 아카데미상 받다 태극기 휘날리다 광해 명량 전부문 휩쓸어야겠     
##  9     9 다시한번 보이다 영화관                                                 
## 10    10 대한민국 봉준호 대단 한국의 문화 자긍심 가지게합니                     
## # ... with 4,014 more rows
(4) 바이그램으로 토큰화하기
bigram_comment <- line_comment %>%
  unnest_tokens(input = sentence,
                output = bigram,
                token = "ngrams",
                n = 2)

bigram_comment
## # A tibble: 23,964 x 2
##       id bigram       
##    <int> <chr>        
##  1     1 우리 좋다    
##  2     1 좋다 생기다  
##  3     1 생기다 기쁘다
##  4     1 기쁘다 행복한
##  5     1 행복한 행복  
##  6     1 행복 축하    
##  7     1 축하 행복    
##  8     1 행복 기쁘다  
##  9     2 기쁘다 시국  
## 10     2 시국 기쁘다  
## # ... with 23,954 more rows

연이어 사용된 단어쌍 빈도 구하기

1. 바이그램 분리하기
# 바이그램 분리하기
bigram_seprated <- bigram_comment %>%
  separate(bigram, c("word1", "word2"), sep = " ")

bigram_seprated
## # A tibble: 23,964 x 3
##       id word1  word2 
##    <int> <chr>  <chr> 
##  1     1 우리   좋다  
##  2     1 좋다   생기다
##  3     1 생기다 기쁘다
##  4     1 기쁘다 행복한
##  5     1 행복한 행복  
##  6     1 행복   축하  
##  7     1 축하   행복  
##  8     1 행복   기쁘다
##  9     2 기쁘다 시국  
## 10     2 시국   기쁘다
## # ... with 23,954 more rows
2. 단어쌍 빈도 구하기
# 단어쌍 빈도 구하기
pair_bigram <- bigram_seprated %>%
  count(word1, word2, sort = T) %>%
  na.omit()

pair_bigram
## # A tibble: 19,782 x 3
##    word1      word2          n
##    <chr>      <chr>      <int>
##  1 봉준호     봉준호       154
##  2 진심       축하          64
##  3 블랙리스트 올리다        62
##  4 봉준호     축하          57
##  5 대단       축하          38
##  6 영화       만들다        31
##  7 축하       봉준호        29
##  8 대박       축하          25
##  9 봉준호     블랙리스트    25
## 10 한국       영화          24
## # ... with 19,772 more rows

na.omit(): 결측치 행 제거: 한 단어로 된 문장은 바이그램으로 토큰화하면 NA가 됨

ex) ‘축하합니다’, ‘멋집니다’

3. 단어쌍 살펴보기
# 동시 출현 단어쌍
pair %>% #<<
  filter(item1 == "대한민국")
## # A tibble: 1,072 x 3
##    item1    item2        n
##    <chr>    <chr>    <dbl>
##  1 대한민국 봉준호      68
##  2 대한민국 축하        54
##  3 대한민국 자랑        43
##  4 대한민국 영화        29
##  5 대한민국 기생충      27
##  6 대한민국 국민        22
##  7 대한민국 세계        16
##  8 대한민국 대단        16
##  9 대한민국 아카데미    16
## 10 대한민국 위상        15
## # ... with 1,062 more rows
# 바이그램 단어쌍
pair_bigram %>% #<<
  filter(word1 == "대한민국")
## # A tibble: 116 x 3
##    word1    word2      n
##    <chr>    <chr>  <int>
##  1 대한민국 국민      21
##  2 대한민국 자랑      13
##  3 대한민국 영화      10
##  4 대한민국 위상       7
##  5 대한민국 만세       6
##  6 대한민국 봉준호     5
##  7 대한민국 국격을     4
##  8 대한민국 문화       4
##  9 대한민국 기생충     3
## 10 대한민국 사람       3
## # ... with 106 more rows

엔그램으로 네트워크 그래프 만들기

# 네트워크 그래프 데이터 만들기
graph_bigram <- pair_bigram %>%
  filter(n >= 8) %>%
  as_tbl_graph()

# 네트워크 그래프 만들기
set.seed(1234)
word_network(graph_bigram)

유의어 통일하고 네트워크 그래프 다시 만들기
  • bigram_seprated의 유의어 통일, 같은 단어 연속 단어쌍 제거

  • 단어쌍 빈도 구하고 결측치 제거

# 유의어 처리
bigram_seprated <- bigram_seprated %>%
  mutate(word1 = ifelse(str_detect(word1, "대단"), "대단", word1),
         word2 = ifelse(str_detect(word2, "대단"), "대단", word2),

         word1 = ifelse(str_detect(word1, "자랑"), "자랑", word1),
         word2 = ifelse(str_detect(word2, "자랑"), "자랑", word2),

         word1 = ifelse(str_detect(word1, "짝짝짝"), "짝짝짝", word1),
         word2 = ifelse(str_detect(word2, "짝짝짝"), "짝짝짝", word2)) %>%

  # 같은 단어 연속 제거
  filter(word1 != word2)

# 단어쌍 빈도 구하기
pair_bigram <- bigram_seprated %>%
  count(word1, word2, sort = T) %>%
  na.omit()
# 네트워크 그래프 데이터 만들기
set.seed(1234)
graph_bigram <- pair_bigram %>%
  filter(n >= 8) %>%
  as_tbl_graph(directed = F) %>%
  mutate(centrality = centrality_degree(),    # 중심성
         group = as.factor(group_infomap()))  # 커뮤니티
# 네트워크 그래프 만들기
set.seed(1234)
ggraph(graph_bigram, layout = "fr") +         # 레이아웃

  geom_edge_link(color = "gray50",            # 엣지 색깔
                 alpha = 0.5) +               # 엣지 명암

  geom_node_point(aes(size = centrality,      # 노드 크기
                      color = group),         # 노드 색깔
                  show.legend = F) +          # 범례 삭제
  scale_size(range = c(4, 8)) +               # 노드 크기 범위

  geom_node_text(aes(label = name),           # 텍스트 표시
                 repel = T,                   # 노드밖 표시
                 size = 5,                    # 텍스트 크기
                 family = "nanumgothic") +    # 폰트

  theme_graph()                               # 배경 삭제

  • 자주 연이어 사용된 단어쌍 중심으로 네트워크 형성

  • 단어의 맥락과 의미를 구체적으로 이해할 수 있음

  • 개별 단어의 빈도는 낮지만 자주 연이어 사용되고 함께 사용할 때 분명한 의미 지니는 단어쌍 발견

    • ex) ‘이미경-부회장’, ‘조국-가족’
파이 계수, 바이그램 네트워크 그래프의 차이점
  • 파이 계수를 이용한 네트워크 그래프

    • 관련성이 큰 단어쌍 중심으로 네트워크 형성

    • 빈도가 낮아도 관련성이 큰 단어 주로 표현

    • 관련성이 작은 노드들이 연결되지 않음

      • 단어 군집이 명확하게 드러남 but 단어들의 전반적인 관계를 파악하기 어려움
  • 바이그램을 이용한 네트워크 그래프

    • 연이어 자주 사용된 단어쌍 중심으로 표현

    • 관련성이 큰 동시에 자주 사용된 단어 주로 표현

    • 노드가 대부분 연결됨

      • 단어 군집이 덜 명확 but 단어들의 전반적인 관계 파악할 수 있음


어떤 방법으로 네트워크 그래프를 만드는 게 좋을까
  • 각 방법의 특징 다르므로 분석 목적에 맞게 선택

  • 세 가지 방법 모두 사용해 분석 결과 비교하면 텍스트를 다각도로 이해할 수 있음

정리하기

1. 동시 출현 단어 분석 - Co-occurrence analysis
# 품사 기준 토큰화
comment_pos <- news_comment %>%
  unnest_tokens(input = reply,
                output = word,
                token = SimplePos22,
                drop = F)

# 명사, 동사, 형용사 추출
comment <- comment_pos %>%
  separate_rows(word, sep = "[+]") %>%
  filter(str_detect(word, "/n|/pv|/pa")) %>%
  mutate(word = ifelse(str_detect(word, "/pv|/pa"),
                       str_replace(word, "/.*$", "다"),
                       str_remove(word, "/.*$"))) %>%
  filter(str_count(word) >= 2) %>%
  arrange(id)
# 단어 동시 출현 빈도 구하기
pair <- comment %>%
  pairwise_count(item = word,
                 feature = id,
                 sort = T)
2. 단어 간 상관 분석 - Phi coefficient
# 파이 계수 구하기
word_cors <- comment %>%
  add_count(word) %>%
  filter(n >= 20) %>%
  pairwise_cor(item = word,
               feature = id,
               sort = T)
3. 연이어 사용된 단어쌍 분석 - n-gram
# 텍스트를 한 행으로 구성
line_comment <- comment %>%
  group_by(id) %>%
  summarise(sentence = paste(word, collapse = " "))

# 바이그램 토큰화
bigram_comment <- line_comment %>%
  unnest_tokens(input = sentence,
                output = bigram,
                token = "ngrams",
                n = 2)

# 바이그램 분리
bigram_seprated <- bigram_comment %>%
  separate(bigram, c("word1", "word2"), sep = " ")
# 단어쌍 빈도 구하기
pair_bigram <- bigram_seprated %>%
  count(word1, word2, sort = T) %>%
  na.omit()
4. 네트워크 그래프 만들기
# 네트워크 그래프 데이터 만들기
set.seed(1234)
graph_comment <- pair_bigram %>%
  filter(n >= 8) %>%
  as_tbl_graph(directed = F) %>%
  mutate(centrality = centrality_degree(),
         group = as.factor(group_infomap()))

# 네트워크 그래프 만들기
set.seed(1234)
ggraph(graph_comment) +
  geom_edge_link() +
  geom_node_point(aes(size = centrality,
                      color = group)) +
  geom_node_text(aes(label = name))